//
//  IDDPersistentCacheManager.m
//  ComJia
//
//  Created by Aslan on 17/02/2017.
//  Copyright © 2017 ComJia. All rights reserved.
//

#import "IDDPersistentCacheManager.h"
#import "IDDError+LocalCache.h"
#import "NSFileManager+IDDAddition.h"
#import "NSString+IDDMD5.h"

@implementation IDDPersistentCacheManager

@synthesize privateCacheDirectory = _privateCacheDirectory;
@synthesize publicCacheDirectory = _publicCacheDirectory;

- (instancetype)initWithRoot:(NSString *)root
{
    self = [super init];
    if (self) {
        NSArray *cachePaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
        if (cachePaths.count == 0) {
            return nil;
        }
        NSString *cachePath = cachePaths[0];
        _rootCachePath = [[cachePath stringByAppendingPathComponent:@"com.iddkit.cache"] stringByAppendingPathComponent:root];
    }
    return self;
}

- (BOOL)canSupportObject:(id)object
{
    return NO;
}

- (BOOL)saveObject:(id)object atPath:(NSString *)path error:(IDDError *__autoreleasing *)error
{
    return NO;
}

- (id)restoreObjectAtPath:(NSString *)path error:(IDDError *__autoreleasing *)error
{
    return nil;
}

- (void)setPrivateCacheDirectory:(NSString *)privateCacheDirectory
{
    _privateCacheDirectory = [privateCacheDirectory copy];
    _privateCachePath = [_rootCachePath stringByAppendingPathComponent:_privateCacheDirectory];
    NSFileManager *fileManager = [[NSFileManager alloc] init];
    // TODO: detect and report error
    [fileManager createDirectoryAtPath:_privateCachePath withIntermediateDirectories:YES attributes:nil error:nil];
}

- (void)setPublicCacheDirectory:(NSString *)publicCacheDirectory
{
    _publicCacheDirectory = [publicCacheDirectory copy];
    _publicCachePath = [_rootCachePath stringByAppendingPathComponent:_publicCacheDirectory];
    NSFileManager *fileManager = [[NSFileManager alloc] init];
    // TODO: detect and report error
    [fileManager createDirectoryAtPath:_publicCachePath withIntermediateDirectories:YES attributes:nil error:nil];
}

// internal method
- (unsigned long long)cacheSizeAtPath:(NSString *)path
{
    NSString *realPath = [_rootCachePath stringByAppendingPathComponent:path.length ? path:@""];
    unsigned long long size = 0;
    NSFileManager *fileManager = [[NSFileManager alloc] init];
    NSError *error = nil;
    NSDictionary *attributes = [fileManager attributesOfItemAtPath:realPath error:&error];
    if (error) {
        return 0;
    }
    NSString *fileType = [attributes fileType];
    if ([fileType isEqualToString:NSFileTypeRegular]) {
        return [attributes fileSize];
    }
    if (![fileType isEqualToString:NSFileTypeDirectory]) {
        return 0;
    }
    NSArray *files = [fileManager subpathsOfDirectoryAtPath:realPath error:&error];
    if (error) {
        return 0;
    }
    for (NSString *subPath in files) {
        NSString *itemPath = [realPath stringByAppendingPathComponent:subPath];
        NSDictionary *attributes = [fileManager attributesOfItemAtPath:itemPath error:&error];
        if (error) {
            NSLog(@"[IDDPersistentCacheManager cacheSize] error: %@", error);
            continue;
        }
        if ([[attributes fileType] isEqualToString:NSFileTypeDirectory]) {
            continue;
        }
        size += [attributes fileSize];
        NSLog(@"size = %llu", size);
    }
    return size;
}

- (unsigned long long)cacheSize
{
    return [self cacheSizeAtPath:nil];
}

- (unsigned long long)privateCacheSizeAtPath:(NSString *)path
{
    path = [_privateCacheDirectory stringByAppendingPathComponent:path ? path:@""];
    return [self cacheSizeAtPath:path];
}

- (unsigned long long)publicCacheSizeAtPath:(NSString *)path
{
    path = [_publicCacheDirectory stringByAppendingPathComponent:path ? path:@""];
    return [self cacheSizeAtPath:path];
}

// internal method
- (void)cleanCacheAtPath:(NSString *)path
{
    NSString *realPath = [_rootCachePath stringByAppendingPathComponent:path.length ? path:@""];
    NSFileManager *fileManager = [[NSFileManager alloc] init];
    [fileManager removeItemAtPathSafely:realPath];
    if ([path isEqualToString:_rootCachePath] ||
        [path isEqualToString:_privateCachePath] ||
        [path isEqualToString:_publicCachePath]) {
        [fileManager createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:nil];
    }
}

- (void)cleanCache
{
    [self cleanCacheAtPath:nil];
}

- (void)cleanPrivateCacheAtPath:(NSString *)path
{
    path = [_privateCacheDirectory stringByAppendingPathComponent:path ? path:@""];
    [self cleanCacheAtPath:path];
}

- (void)cleanPublicCacheAtPath:(NSString *)path
{
    path = [_publicCacheDirectory stringByAppendingPathComponent:path ? path:@""];
    [self cleanCacheAtPath:path];
}

- (IDDCacheEntity *)setEntity:(IDDCacheEntity *)entity error:(IDDError *__autoreleasing *)error
{
    if (![self canSupportObject:entity.cache]) {
#ifdef DEBUG
        [NSException raise:@"setNonSupportedPersistentCache" format:@"persistent cache not supported"];
        return nil;
#else
        if (error) {
            *error = cacheErrorWithCode(IDDErrorCacheDomainUncacheableObject, nil);
        }
        return nil;
#endif
    }
    
    if (error) {
        *error = nil;
    }
    NSString *cacheDirectory = nil;
    if (entity.type == IDDCacheEntityTypePrivate) {
        cacheDirectory = _privateCachePath;
    } else if (entity.type == IDDCacheEntityTypePublic) {
        cacheDirectory = _publicCachePath;
    }
    NSFileManager *fileManager = [[NSFileManager alloc] init];
    NSString *parentPath = [cacheDirectory stringByAppendingPathComponent:entity.path];
    NSError *thisError = nil;
#ifdef DEBUG
    NSString *cacheFilePath = [parentPath stringByAppendingPathComponent:entity.key];
#else
    NSString *cacheFilePath = [parentPath stringByAppendingPathComponent:[entity.key md5]];
#endif
    if (!entity.cache) {
        [fileManager removeItemAtPath:cacheFilePath error:&thisError];
        if (!thisError) {
            return entity;
        }
        if ([thisError.domain isEqualToString:NSCocoaErrorDomain] && thisError.code == NSFileNoSuchFileError) {
            return entity;
        } else {
            if (error) {
                *error = cacheErrorWithCode(IDDErrorCacheDomainRemoveItem, thisError);
            }
            return nil;
        }
    }
    BOOL created = [fileManager createDirectoryAtPath:parentPath withIntermediateDirectories:YES attributes:nil error:&thisError];
    if (!created) {
        if (error) {
            *error = cacheErrorWithCode(IDDErrorCacheDomainFilesystemError, thisError);
        }
        return nil;
    }
    if ([self saveObject:entity.cache atPath:cacheFilePath error:error]) {
        return entity;
    }
    return nil;
}

- (IDDCacheEntity *)getEntityAtPath:(NSString *)path error:(IDDError *__autoreleasing *)error
{
    if (error) {
        *error = nil;
    }
    IDDError *thisError = nil;
    id cache = [self restoreObjectAtPath:path error:&thisError];
    if (thisError) {
        if (error) {
            *error = thisError;
        }
        return nil;
    }
    if (!cache) {
        return nil;
    }
    NSFileManager *fileManager = [[NSFileManager alloc] init]; // do not use defaultManager, for we need to support concurrency
    NSError *nsError = nil;
    NSDictionary *attributes = [fileManager attributesOfItemAtPath:path error:&nsError];
    if (nsError) {
        if (error) {
            *error = cacheErrorWithCode(IDDErrorCacheDomainFilesystemError, thisError);
        }
        return nil;
    }
    IDDCacheEntity *entity = [[IDDCacheEntity alloc] init];
    entity.cache = cache;
    entity.date = [attributes fileModificationDate];
    return entity;
}

- (id)privateCacheForKey:(NSString *)key error:(IDDError *__autoreleasing *)error
{
    return [self privateCacheForKey:key atPath:nil error:error];
}

- (IDDCacheEntity *)privateCacheForKey:(NSString *)key atPath:(NSString *)path error:(IDDError *__autoreleasing *)error
{
#ifdef DEBUG
    NSString *cacheFilePath = [[_privateCachePath stringByAppendingPathComponent:path] stringByAppendingPathComponent:key];
#else
    NSString *cacheFilePath = [[_privateCachePath stringByAppendingPathComponent:path] stringByAppendingPathComponent:[key md5]];
#endif
    IDDCacheEntity *entity = [self getEntityAtPath:cacheFilePath error:error];
    if (!entity) {
        return nil;
    }
    entity.key = key;
    entity.path = path;
    entity.type = IDDCacheEntityTypePrivate;
    return entity;
}

- (void)privateCacheForKey:(NSString *)key finished:(IDDCacheManagerCallback)finished
{
    [self privateCacheForKey:key atPath:nil finished:finished];
}

- (void)privateCacheForKey:(NSString *)key atPath:(NSString *)path finished:(IDDCacheManagerCallback)finished
{
    IDDError *thisError = nil;
#ifdef DEBUG
    NSString *cacheFilePath = [[_privateCachePath stringByAppendingPathComponent:path] stringByAppendingPathComponent:key];
#else
    NSString *cacheFilePath = [[_privateCachePath stringByAppendingPathComponent:path] stringByAppendingPathComponent:[key md5]];
#endif
    IDDCacheEntity *entity = [self getEntityAtPath:cacheFilePath error:&thisError];
    if (thisError) {
        finished(nil, thisError);
    } else {
        entity.key = key;
        entity.path = path;
        entity.type = IDDCacheEntityTypePrivate;
        finished(entity, nil);
    }
}

- (IDDCacheEntity *)setPrivateCache:(id)cache forKey:(NSString *)key error:(IDDError *__autoreleasing *)error
{
    return [self setPrivateCache:cache forKey:key atPath:nil error:error];
}

- (IDDCacheEntity *)setPrivateCache:(id)cache forKey:(NSString *)key atPath:(NSString *)path error:(IDDError *__autoreleasing *)error
{
    IDDCacheEntity *entity = [[IDDCacheEntity alloc] initWithCache:cache forKey:key atPath:path andType:IDDCacheEntityTypePrivate];
    return [self setEntity:entity error:error];
}

- (IDDCacheEntity *)publicCacheForKey:(NSString *)key error:(IDDError *__autoreleasing *)error
{
    return [self publicCacheForKey:key atPath:nil error:error];
}

- (IDDCacheEntity *)publicCacheForKey:(NSString *)key atPath:(NSString *)path error:(IDDError *__autoreleasing *)error
{
#if DEBUG || TEST
    NSString *cacheFilePath = [[_publicCachePath stringByAppendingPathComponent:path] stringByAppendingPathComponent:key];
#else
    NSString *cacheFilePath = [[_publicCachePath stringByAppendingPathComponent:path] stringByAppendingPathComponent:[key md5]];
#endif
    IDDCacheEntity *entity = [self getEntityAtPath:cacheFilePath error:error];
    if (!entity) {
        return nil;
    }
    entity.key = key;
    entity.path = path;
    entity.type = IDDCacheEntityTypePublic;
    return entity;
}

- (void)publicCacheForKey:(NSString *)key finished:(IDDCacheManagerCallback)finished
{
    [self publicCacheForKey:key atPath:nil finished:finished];
}

- (void)publicCacheForKey:(NSString *)key atPath:(NSString *)path finished:(IDDCacheManagerCallback)finished
{
    IDDError *thisError = nil;
#ifdef DEBUG
    NSString *cacheFilePath = [[_publicCachePath stringByAppendingPathComponent:path] stringByAppendingPathComponent:key];
#else
    NSString *cacheFilePath = [[_publicCachePath stringByAppendingPathComponent:path] stringByAppendingPathComponent:[key md5]];
#endif
    IDDCacheEntity *entity = [self getEntityAtPath:cacheFilePath error:&thisError];
    if (thisError) {
        finished(nil, thisError);
    } else {
        entity.key = key;
        entity.path = path;
        entity.type = IDDCacheEntityTypePublic;
        finished(entity, nil);
    }
}

- (IDDCacheEntity *)setPublicCache:(id)cache forKey:(NSString *)key error:(IDDError *__autoreleasing *)error
{
    return [self setPublicCache:cache forKey:key atPath:nil error:error];
}

- (IDDCacheEntity *)setPublicCache:(id)cache forKey:(NSString *)key atPath:(NSString *)path error:(IDDError *__autoreleasing *)error
{
    
    IDDCacheEntity *entity = [[IDDCacheEntity alloc] initWithCache:cache forKey:[key md5] atPath:path andType:IDDCacheEntityTypePublic];
    return [self setEntity:entity error:error];
}

@end
